package com.loadburn.heron.converter;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import com.google.common.primitives.Primitives;
import com.google.inject.Inject;
import com.loadburn.heron.utils.generics.GenericsUtils;

import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.Arrays;
import java.util.Collection;
import java.util.Set;

import static com.loadburn.heron.utils.generics.GenericsUtils.*;


/**
 * @author slacrey (scstlinfeng@yahoo.com)
 *         Date: 13-10-26
 */
public class TypeConverterImpl implements TypeConverter, ConverterRegistry {

    Multimap<Type, Converter<?, ?>> convertersBySource = ArrayListMultimap.create();
    Multimap<Type, Converter<?, ?>> convertersByTarget = ArrayListMultimap.create();

    Multimap<SourceAndTarget, Converter<?, ?>> convertersBySourceAndTarget = ArrayListMultimap.create();
    private static final TypeVariable<? extends Class<?>> sourceTypeParameter = Converter.class.getTypeParameters()[0];
    private static final TypeVariable<? extends Class<?>> targetTypeParameter = Converter.class.getTypeParameters()[1];

    @Inject
    public TypeConverterImpl(@SuppressWarnings("rawtypes") Set<Converter> converters) {
        for (Converter<?, ?> converter : converters) {
            register(converter);
        }
    }

    @Override
    public void register(Converter<?, ?> converter) {
        Type sourceType = sourceType(converter);
        Type targetType = targetType(converter);
        convertersBySource.put(sourceType, converter);
        convertersByTarget.put(targetType, converter);
        convertersBySourceAndTarget.put(new SourceAndTarget(sourceType, targetType), converter);
    }

    public static Type targetType(Converter<?, ?> converter) {
        return getTypeParameter(converter.getClass(), targetTypeParameter);
    }

    public static Type sourceType(Converter<?, ?> converter) {
        return getTypeParameter(converter.getClass(), sourceTypeParameter);
    }

    @Override
    public Multimap<Type, Converter<?, ?>> getConvertersByTarget() {
        return convertersByTarget;
    }

    @Override
    public Multimap<Type, Converter<?, ?>> getConvertersBySource() {
        return convertersBySource;
    }

    @Override
    public Collection<Converter<?, ?>> converter(Type source, Type target) {
        SourceAndTarget key = new SourceAndTarget(source, target);
        return convertersBySourceAndTarget.get(key);
    }

    @SuppressWarnings("unchecked")
    @Override
    public <T> T convert(Object source, Type type) {
        // special case for handling a null source values
        if (source == null) {
            return (T) nullValue(type);
        }

        // check we already have the exact type
        if (source.getClass() == type) {
            return (T) source;
        }

        // check if we already have a sub type
        if (GenericsUtils.isSuperType(type, source.getClass())) {
            return (T) source;
        }

        // special case for handling empty string
        if ("".equals(source) && type != String.class && isEmptyStringNull()) {
            return null;
        }

        Type sourceType = source.getClass();
        Class<?> sourceClass = erase(sourceType);

        // converter of all array types to collections
        if (sourceClass.isArray() && GenericsUtils.isSuperType(Collection.class, type)) {
            return (T) Arrays.asList(source);
        }

        // converter of all collections to arrays
        Class<?> targetClass = erase(type);
        if (Collection.class.isAssignableFrom(sourceClass) && targetClass.isArray()) {
            // TODO: convert collections to arrays
            throw new UnsupportedOperationException("Not implemented yet");
        }

        // use primitive wrapper types
        if (type instanceof Class<?> && ((Class<?>) type).isPrimitive()) {
            type = Primitives.wrap((Class<?>) type);
        }


        // look for converters for exact types or super types
        Object result = null;
        do {

            // first try to find a converter in the forward direction
            SourceAndTarget key = new SourceAndTarget(sourceType, type);
            Collection<Converter<?, ?>> forwards = convertersBySourceAndTarget.get(key);

            // stop path the first converter that returns non-null
            for (Converter<?, ?> forward : forwards)
                if ((result = typeSafeTo(forward, source)) != null) break;

            if (result == null) {
                // try the reverse direction (target to source)
                Collection<Converter<?, ?>> reverses = convertersBySourceAndTarget.get(key.reverse());

                // stop path the first converter that returns non-null
                for (Converter<?, ?> reverse : reverses)
                    if ((result = typeSafeFrom(reverse, source)) != null) break;
            }

            // we have no more super classes to try
            if (sourceType == Object.class) break;

            // try every super type of the source
            Class<?> superClass = erase(sourceType).getSuperclass();
            sourceType = getExactSuperType(sourceType, superClass);
        } while (result == null);

        if (result == null)
            throw new IllegalStateException("Cannot convert " + source.getClass() + " to " + type);

        return (T) result;
    }

    protected boolean isEmptyStringNull() {
        return true;
    }

    protected Object nullValue(Type type) {
        if (type == String.class) {
            return "";
        } else return null;
    }

    @SuppressWarnings("unchecked")
    public static <T, S> T typeSafeTo(Converter<?, ?> converter, S source) {
        return ((Converter<S, T>) converter).to(source);
    }

    @SuppressWarnings("unchecked")
    public static <T, S> S typeSafeFrom(Converter<?, ?> converter, T source) {
        return ((Converter<S, T>) converter).from(source);
    }


    private static final class SourceAndTarget {
        private Type source;
        private Type target;

        public SourceAndTarget(Type source, Type target) {
            this.source = source;
            this.target = target;
        }

        public SourceAndTarget reverse() {
            return new SourceAndTarget(target, source);
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + ((source == null) ? 0 : source.hashCode());
            result = prime * result + ((target == null) ? 0 : target.hashCode());
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            SourceAndTarget other = (SourceAndTarget) obj;
            if (source == null) {
                if (other.source != null)
                    return false;
            } else if (!source.equals(other.source))
                return false;
            if (target == null) {
                if (other.target != null)
                    return false;
            } else if (!target.equals(other.target))
                return false;
            return true;
        }
    }

}
